################################################################################
#### Firewall.sh - a little firewall script
###  Version: 1.0b
##
#    Firewall.sh is a little script to configure a firewall using a simple
#    syntax.
##
###  GNU GPL - You can use this code as you want but become it close software.
#### Aitor Pérez Iturri <aitor.iturri@gmail.com>  -  2010 
################################################################################

################################################################################
## USAGE:
##      Firewall.sh [start | stop | sniff]
##
##      start: 
##          starts the firewall, adding rules into your netfilter.
##      stop:
##          stops the firewall, deleting the rules from your netfilter.
##      man-in-the-middle:
##          allows you to redirect all trafic from some interface, matching
##          your own criteria to another host in the network. Usseful to 
##          capture data from open wireleŝ point access. Is fun to catch the 
##          intruder while is surfing trought your network ;).
##          Read the manual to see how to configure your network for this mode.
##
## CONFIGURE:
##      To configure Firewall.sh you may first set the PREFIX to the location
##      where the config files are located, by default '/etc/Firewall.conf'.
##      You may also define the IFACES variable to let the script to know in
##      which interfaces you want to filter. The format is a list of logical
##      names and real interfaces, something like:
##              'IFACES="WLAN:br0 LAN:vlan2 WAN:vlan1"'
##  
##      The script will create 3 chains for each device defined:
##              input_DEV
##              output_DEV
##              forwarding_DEV
##
##
################################################################################

## Configuration variables #####################################################
PREFIX=$PWD/Firewall.conf
IFACES="WLAN:br0 LAN:vlan2 WAN:vlan1"
BUSYBOX=true
DEBUG=0
################################################################################

## You shouldn't modify from here, save that you REALLY know what you are doing.

TABLES="mangle:PREROUTING;INPUT;FORWARD;OUTPUT;POSTROUTING filter:INPUT;FORWARD;OUTPUT" 
TABLES="$TABLES nat:PREROUTING;OUTPUT;POSTROUTING"
#TABLES="mangle:PREROUTING;INPUT;FORWARD;OUTPUT;POSTROUTING"

BUSYBOX_TMP_FILE="/tmp/busybox_tmp"
RULES_TMP_FILE="/tmp/rules_tmp"

debug () {
	[ "${DEBUG}_NULL" != "_NULL" ] && {
		echo " [DEBUG] $1" 
		return 0
	} || return 1
}

error () {
	echo " [ERROR] $1"
}

exit_on_error () {
	error=$2
	errn=$1
	[ "${error}_NULL" != "_NULL" ] && echo -e " [ERROR] $error"
	[ $errn -ne 3 ] && {
		iptables -F
		iptables -X
	}
	exit $errn
}

configure_chain () {
	debug "Configuring $2 [$1]"
    [ -f $1 ] && {
        FW_CONFFILE=$1 
        export FW_CONFMODE=USER
    } || {
        FW_CONFFILE=/dev/null
		debug "Configuring chain '$2' in 'default' mode"
        export FW_CONFMODE=DEFAULT
    }
	
    FW_CHAIN=$2;
	FW_TABLE=$3
    FW_RULE_MODE=$4
	FW_DEBUG=$DEBUG
    export FW_RULE_MODE
    export FW_CHAIN
	export FW_TABLE
	export FW_DEBUG

    # Busybox support here
    [ "${BUSYBOX}_NULL" != "_NULL" ] && {
    	[ -f ${BUSYBOX_TMP_FILE} ] && rm -rf ${BUSYBOX_TMP_FILE}
    	echo FW_CHAIN=$FW_CHAIN >> $BUSYBOX_TMP_FILE
		echo FW_TABLE=$FW_TABLE >> $BUSYBOX_TMP_FILE
    	echo FW_DEBUG=$FW_DEBUG >> $BUSYBOX_TMP_FILE
    	echo FW_CONFMODE=$FW_CONFMODE >> $BUSYBOX_TMP_FILE
    	echo FW_RULE_MODE=$FW_RULE_MODE >> $BUSYBOX_TMP_FILE
		echo RULES_TMP_FILE=$RULES_TMP_FILE >> $BUSYBOX_TMP_FILE
    }

    # export variables to let them be seen from awk
	_IFS=${IFS}
	IFS=$' '
    for i in ${IFACES}; do
        iface=${i%:*}
        device=${i#*:}

        # More busybox support!
        [ "${BUSYBOX}_NULL" != "_NULL" ] && {
        	echo $iface=$device >> $BUSYBOX_TMP_FILE
        }
        eval "export $iface=$device"
    done
    ## awk program
    ## Table Struct for rules, matches and arguments
    ## rules table contains:
    ##      #rule, "iface" = Interface
    ##      #rule, "target" = Target
    ##      #rule, "matches" = # of matches in the rule
    ##
    ## matches table contains:
    ##      #rule, #match, "match" = name of the match
    ##      #rule, #match, "arguments" = # of arguments in he match
    ##
    ## arguments table contains:
    ##      #rule, #match, #argument = argument to be passed to match
    ##
    awk -v BUSYBOX=$BUSYBOX -v BUSYBOX_TMP_FILE=$BUSYBOX_TMP_FILE \
    'BEGIN {
        # rules: Array to store defined rules
        rulesn = 1;

        # Support for busybox, now the awk side :)
        if (BUSYBOX == "true") {
        	while ((getline line < BUSYBOX_TMP_FILE) > 0) {
        		split(line,var,"=");
        		ENVIRON[var[1]]=var[2];
        	}
        	chain = ENVIRON["FW_CHAIN"];
			table = ENVIRON["FW_TABLE"];
        	DEBUG = ENVIRON["FW_DEBUG"];
        	confmode = ENVIRON["FW_CONFMODE"];
            RULEMODE = ENVIRON["FW_RULE_MODE"];
			RULES_TMP_FILE = ENVIRON["RULES_TMP_FILE"];
        } else {
	        chain = ENVIRON["FW_CHAIN"];
			table = ENVIRON["FW_TABLE"];
        	DEBUG = ENVIRON["FW_DEBUG"];
			print("DEBUG " DEBUG)
	        confmode = ENVIRON["FW_CONFMODE"];
            RULEMODE = ENVIRON["FW_RULE_MODE"];
			RULES_TMP_FILE = ENVIRON["RULES_TMP_FILE"];
        }
        if (chain ~ /^input/) 
            mode = "input";
        else
            mode = "output";
    }
    # Define some handy functions
	function get_mode(chain,     c) {
		c=tolower(chain)
		if (c ~ /^input/ || c ~ /^prerouting/)
			return "-o";
		if (c ~ /^output/ || c ~ /^forward/ || c ~ /^postrouting/)
		return "-i";
	};
    ## Dump defined rules to stdout
    function dump_rules() {
        for (i=1;i<rulesn;i++) {
            print("`---> Interface = " rules[i,"iface"])
            print("`---> Target = " rules[i, "target"])
            print("`---> Matches = " rules[i, "matches"])
            for (j=1;j<=rules[i,"matches"];j++) {
                print("  `---> Match = " matches[i, j, "match"]);
                print("  `---> Arguments = " matches[i,j, "arguments"]);
                for (k=1;k<=matches[i,j,"arguments"];k++) {
                    print("     `---> Argument: " arguments[i,j,k])
                }
            }
        }
    };
    ##
    function execute(cmd) {
        if (DEBUG) {
            print(" [DEBUG] +++++ Executing: " cmd);
            (cmd " ;echo $?") | getline output;
        }
        else 
            (cmd " 2>/dev/null ; echo $?") | getline output;
        close(cmd);

        if (output != 0) {
            if (DEBUG) {
            } else {
                print(" [ERROR] Could not execute command:  \n\t\t\"" cmd"")
                print("         Please try to run in debug mode to " \
                    "get the output");
            }
            return 1;
        }
    }
    ## rule_to_iptables
    ##      function to convert an awk rule into an iptables cmd
    ##      arguments: 
    ##          r -> #rule
    ##          mode -> ["start" | "stop"]
    function rule_to_iptables(r, operation,            #arguments
            cmd) {    #local variables

        if (operation == "start")
                cmd = "iptables -t " table " -A " chain; # Append mode
            else
                cmd = "iptables -t " table " -D " chain; # Delete mode

		if (tolower(chain) ~ /^dorward/)
			cmd = cmd " -i " rules[r, "iface"];

        
        for (m=1;m<=rules[r, "matches"];m++) {
          #  if (matches[r,m,"match"] ~ "^m@")
          #      cmd = cmd " -m " gensub("^m@","","g",matches[r, m, "match"]);
          #  else if (matches[r,m,"match"] ~ "^p@")
          #      cmd = cmd " -p " gensub("^p@","","g",matches[r, m, "match"]);
				o = gensub(/^(.+)@.+/,"\\1", "g", matches[r, m, "match"])
				cmd = cmd " -" o " " gensub(o "@","","g",matches[r, m, "match"]);
			
            for (a=1; a<=matches[r, m, "arguments"]; a++) {
                # Replace "." and "=" by " ".
                cmd = cmd " --" gensub("[=.]"," ","g",arguments[r, m, a])
            }
        }

        if (rules[r,"target"] ~ /.+>.+/) {
            cmd = cmd " -j " gensub(/>.+$/,"","g",rules[r, "target"])
            cmd = cmd " --" gensub(/^.+>/,"","g",rules[r, "target"])
        } 
        else 
            cmd = cmd " -j " rules[r, "target"];
		
        execute(cmd);
    };
    /policy:/ { print(" +++ Setting default policy to: " $2)
        defaultpolicy = $2;
    }
    /^rule:/ {
		print " +++ Creating new " $0
        iface = ENVIRON[$2];
        if (length(iface) == 0 && RULEMODE == "directional") {
			if ($2 == "+")
				iface = "any"
			else
	            iface = "UNKNOW"
        }
        
        matchsn=0;

        if (tolower(chain) ~ /^forward/) {
			iface = ENVIRON[$2];
            target = $3;
            iter = 4;
        }
        else {
            target = $2;
            iter = 3;
        }

        for (i=iter;i<=NF;i++) { # Iterate over matches for rule
            split($i, rule, ">"); # Split match and match arguments
            matchn = i - (iter - 1); # Store the index of the current match
            argumentsn = split(rule[2], args, ";"); # Split arguments and values
            # Store the match and its # of args into the matches table
            matches[rulesn, matchn, "match" ] = rule[1];
            matches[rulesn, matchn, "arguments"] = argumentsn;
            # Iterate over Arguments for the match
            for (j=1;j<=argumentsn;j++) {
                # Store the argument and its value into the arguments table
#                #print(args[j] " = " args[j+1])
                arguments[rulesn, matchn, j] = args[j]
            }
        }
		rules[rulesn, "rule"] = $0
        rules[rulesn, "iface"] = iface;
        rules[rulesn, "oface"] = oface;
        rules[rulesn, "target"] = target;
        rules[rulesn, "matches"] = NF - (iter - 1);
        rulesn++;
    }
    /#/ {} # Just a comment, do nothing, amazed?¿?¿?¿
    END {
        if (confmode == "USER") {
            for (i=1;i<rulesn;i++) {
                rule_to_iptables(i, "start");
            }
        }
    }
    ' $FW_CONFFILE
	return $?
}

execute () {
    cmd="$1"
    silent_mode=$2

	debug "Executing '$cmd'"
    [ "${silent_mode}_NULL" != "_NULL" ] && {
        eval $cmd > /dev/null 2>&1
        return $?
    }

#    [ "${FW_DEBUG}_NULL" != "_NULL" ] && {
#        echo ""
#        echo "Executing: $cmd"
#        echo ""
#        eval $cmd
#    } || {
#        eval $cmd >/dev/null 2>&1
#    }
#	[ "${silent_mode}_NULL" != "_NULL" ] && {
#		eval $cmd > /dev/null 2>&1
#	} || {
		output=`eval $cmd 2>&1`
#	}
	errcode=$?

    [ $errcode -gt 0 ] && {
        echo " [ERROR] Could not execute command  '$cmd'"
	#	debug "$output" || {
	#        echo "         Please, try to run with DEBUG mode ON to see more details"
	#	}
		error "$output"
		return $errcode
    }
	return 0
}

chain_mode () {
	case "$1" in
		"input"|"prerouting")
			echo "-i";
			;;
		"output"|"forward"|"postrouting")
			echo "-o";
			;;
	esac		
}

#
#
# TODO: Check for existence of $table and $chain
do_chain () {
	table=$1
	chain=$2
	chain_lc=`echo $chain | tr A-Z a-z`
	echo " + Populating chain " $table":"$chain
	confchain=${PREFIX}/${table}/${chain_lc}.conf
	configure_chain ${confchain} ${chain} ${table} "directional"
	_IFS=${IFS}
	IFS=$' '
	for i in ${IFACES}; do
		iface=${i%:*}
		device=${i#*:}
		echo " + Adding ${chain_lc}_${iface}"
		subchain=${chain_lc}_${iface}
		confchain=${PREFIX}/${table}/${subchain}.conf
		execute "iptables -t ${table} -N ${subchain}" true || exit_on_error $?
		execute "iptables -t ${table} -F ${subchain}" true || exit_on_error $?
		mode=`chain_mode ${chain_lc}`
		execute "iptables -t ${table} -A ${chain} ${mode} ${device} -j ${subchain}" || exit_on_error $?
		[ "$table" = "nat" -a "$chain_lc" = "postrouting" ] && {
			configure_chain ${confchain} ${subchain} ${table}

		} || {
			configure_chain ${confchain} ${subchain} ${table} "directional"
		}
	done
	IFS=${_IFS}
}

do_tables () {
	for t in ${TABLES}; do
		table=${t%:*}
		chains=${t#*:}
		execute "iptables -t ${table} -F" || exit_on_error $?
		execute "iptables -t ${table} -X" || exit_on_error $?
		IFS=$';'
		for chain in ${chains}; do
			do_chain $table $chain
		done
		unset IFS
	done
}

do_table () {
	for t in ${TABLES}; do
		table=${t%:*}
		chains=${t#*:}
		[ "${table}" = "$1" ] && {
			execute "iptables -t ${table} -F" || exit_on_error $? 
			execute "iptables -t ${table} -X" || exit_on_error $?
			IFS=$';'
			for chain in ${chains}; do
				do_chain $table $chain
			done
			unset IFS
		}
	done
}

usage () {
	echo -e "Usage: Firewall.sh [table] [chain]"
	exit
}

## main(), we need a place from where to start!
main () {
	[ "$1" = "help" ] && usage

	table=$1
	chain=$2

	if [ "${table}_NULL" != "_NULL" ]; then
		if [ "${chain}_NULL" != "_NULL" ]; then
			do_chain ${table} ${chain}
		else
			do_table ${table}
		fi
	else
		do_tables
	fi
}

main $@
